Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit da18029022eab264102676d56601e14f8e254be5


Parents : c206569
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-06-02T09:20:06-05:00

feat(relay): implement member moderation features and update UI with keep-alive support for routing

Changes
Diff

diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index be8241b6..a2dcf31f 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -582,7 +582,24 @@
</div>
<div class="flex flex-1 min-w-0 overflow-hidden">
- <RouterView class="flex-1 min-w-0 h-full" />
+ <RouterView v-slot="{ Component, route }" class="flex-1 min-w-0 h-full">
+ <template v-if="Component">
+ <KeepAlive>
+ <component
+ :is="Component"
+ v-if="route.meta.keepAlive"
+ :key="route.name"
+ class="flex-1 min-w-0 h-full"
+ />
+ </KeepAlive>
+ <component
+ :is="Component"
+ v-if="!route.meta.keepAlive"
+ :key="route.fullPath"
+ class="flex-1 min-w-0 h-full"
+ />
+ </template>
+ </RouterView>
</div>
</div>
</template>

diff --git a/meshchatx/src/frontend/components/relay/RelayHostMembersModal.vue b/meshchatx/src/frontend/components/relay/RelayHostMembersModal.vue
index bf223ad9..2c55c3f9 100644
--- a/meshchatx/src/frontend/components/relay/RelayHostMembersModal.vue
+++ b/meshchatx/src/frontend/components/relay/RelayHostMembersModal.vue
@@ -3,11 +3,11 @@
<template>
<div
v-if="open"
- class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/50 sm:p-4 md:p-6"
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="$emit('close')"
>
<div
- class="flex h-[100dvh] w-full flex-col border-sem-border-card bg-sem-surface shadow-xl sm:h-auto sm:max-h-[min(92vh,900px)] sm:rounded-2xl sm:border lg:max-w-6xl"
+ class="flex h-[min(100dvh-2rem,900px)] w-full max-w-6xl flex-col rounded-2xl border border-sem-border-card bg-sem-surface shadow-xl"
role="dialog"
:aria-label="title"
>
@@ -195,6 +195,7 @@ export default {
messagesLoading: false,
isNarrow: false,
mq: null,
+ localIdentityHash: "",
};
},
computed: {
@@ -224,6 +225,7 @@ export default {
this.search = "";
this.selectedMember = null;
this.memberMessages = [];
+ this.ensureLocalIdentity();
this.fetchMembers();
}
},
@@ -280,16 +282,72 @@ export default {
this.messagesLoading = false;
}
},
+ async ensureLocalIdentity() {
+ if (this.localIdentityHash) {
+ return;
+ }
+ try {
+ const response = await window.api.get("/api/v1/config");
+ const hash = response.data?.identity_hash;
+ if (typeof hash === "string" && hash.trim()) {
+ this.localIdentityHash = hash.trim().toLowerCase();
+ }
+ } catch {
+ // config may be unavailable in tests
+ }
+ },
+ async resolveModerationRoom(member, action) {
+ if (this.room) {
+ return this.room;
+ }
+ const needsRoom = action === "kick" || action === "room_ban";
+ if (!needsRoom) {
+ return null;
+ }
+ const rooms = (member.rooms || []).filter((r) => typeof r === "string" && r.trim());
+ if (rooms.length === 0) {
+ ToastUtils.warning(this.$t("relay_chat.host_kick_no_room"));
+ return null;
+ }
+ if (rooms.length === 1) {
+ return rooms[0];
+ }
+ const entered = await DialogUtils.prompt(
+ this.$t("relay_chat.host_kick_pick_room", {
+ name: member.name,
+ rooms: rooms.map((r) => "#" + r).join(", "),
+ })
+ );
+ if (!entered) {
+ return null;
+ }
+ const norm = entered.trim().replace(/^#/, "");
+ const match = rooms.find((r) => r.toLowerCase() === norm.toLowerCase());
+ if (!match) {
+ ToastUtils.warning(this.$t("relay_chat.host_kick_room_invalid"));
+ return null;
+ }
+ return match;
+ },
async moderate(member, action) {
if (!this.hub?.id || !member?.hash) {
return;
}
+ await this.ensureLocalIdentity();
+ if (this.localIdentityHash && member.hash.toLowerCase() === this.localIdentityHash) {
+ ToastUtils.warning(this.$t("relay_chat.host_cannot_moderate_self"));
+ return;
+ }
+ const room = await this.resolveModerationRoom(member, action);
+ if ((action === "kick" || action === "room_ban") && !room) {
+ return;
+ }
const labels = {
- kick: this.$t("relay_chat.host_kick_confirm", { name: member.name }),
+ kick: this.$t("relay_chat.host_kick_confirm", { name: member.name, room }),
ban: this.$t("relay_chat.host_ban_confirm", { name: member.name }),
room_ban: this.$t("relay_chat.host_room_ban_confirm", {
name: member.name,
- room: this.room,
+ room,
}),
};
const confirmed = await DialogUtils.confirm(labels[action] || "");
@@ -300,7 +358,7 @@ export default {
await window.api.post(`/api/v1/rrc/servers/${this.hub.id}/moderate`, {
action,
peer: member.hash,
- room: this.room || undefined,
+ room: room || undefined,
});
ToastUtils.success(this.$t("common.success"));
this.$emit("refresh");

diff --git a/meshchatx/src/frontend/components/relay/RelayHostRoomsModal.vue b/meshchatx/src/frontend/components/relay/RelayHostRoomsModal.vue
index 75052c97..4025ac8e 100644
--- a/meshchatx/src/frontend/components/relay/RelayHostRoomsModal.vue
+++ b/meshchatx/src/frontend/components/relay/RelayHostRoomsModal.vue
@@ -3,11 +3,11 @@
<template>
<div
v-if="open"
- class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/50 sm:p-4 md:p-6"
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="$emit('close')"
>
<div
- class="flex h-[100dvh] w-full flex-col border-sem-border-card bg-sem-surface shadow-xl sm:h-auto sm:max-h-[min(92vh,900px)] sm:rounded-2xl sm:border lg:max-w-6xl"
+ class="flex h-[min(100dvh-2rem,900px)] w-full max-w-6xl flex-col rounded-2xl border border-sem-border-card bg-sem-surface shadow-xl"
role="dialog"
:aria-label="title"
>

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index db0d8056..40ae66d5 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -2881,22 +2881,48 @@
"show_members": "Mitglieder anzeigen",
"hide_members": "Mitglieder ausblenden",
"discovery_refreshed": "Entdeckung aktualisiert",
- "host_members_search": "Search members by name, hash, or room...",
- "host_members_select": "Select a member to view messages and moderate",
- "host_no_messages": "No messages from this user in the log yet",
- "host_ban_hub": "Ban from hub",
- "host_kick_confirm": "Kick {name} from the current room?",
- "host_ban_confirm": "Ban {name} from this hub?",
- "host_room_ban_confirm": "Ban {name} from #{room}?",
- "host_manage_rooms": "Manage rooms",
- "host_rooms_modal_title": "Rooms — {hub}",
- "host_rooms_select": "Select a room to view activity",
- "host_room_activity": "Recent activity",
- "host_no_activity": "No recent activity in this room",
- "search_messages_placeholder": "Search (AND, OR, DATE:2026-06-02)...",
- "search_empty_hint": "Search this room",
- "search_syntax_hint": "Examples: hello world · alice OR bob · DATE:today · DATE:2026-06-02",
- "members_search_placeholder": "Search members...",
- "members_search_no_results": "No members match your search."
+ "host_members_search": "Mitglieder nach Name, Hash oder Raum suchen...",
+ "host_members_select": "Mitglied auswählen, um Nachrichten anzuzeigen und zu moderieren",
+ "host_no_messages": "Noch keine Nachrichten von diesem Benutzer im Protokoll",
+ "host_ban_hub": "Vom Hub sperren",
+ "host_kick_confirm": "{name} aus #{room} entfernen?",
+ "host_kick_pick_room": "Aus welchem Raum soll {name} entfernt werden? Eingeben: {rooms}",
+ "host_kick_no_room": "Dieser Benutzer ist in keinem Raum und kann nicht entfernt werden.",
+ "host_kick_room_invalid": "Raum nicht gefunden. Wähle einen der aufgeführten Räume.",
+ "host_cannot_moderate_self": "Du kannst deine eigene Verbindung nicht moderieren.",
+ "host_ban_confirm": "{name} von diesem Hub sperren?",
+ "host_room_ban_confirm": "{name} aus #{room} sperren?",
+ "host_manage_rooms": "Räume verwalten",
+ "host_rooms_modal_title": "Räume — {hub}",
+ "host_rooms_select": "Raum auswählen, um Aktivität anzuzeigen",
+ "host_room_activity": "Letzte Aktivität",
+ "host_no_activity": "Keine kürzliche Aktivität in diesem Raum",
+ "search_messages_placeholder": "Suchen (AND, OR, DATUM:2026-06-02)...",
+ "search_empty_hint": "Diesen Raum durchsuchen",
+ "search_syntax_hint": "Beispiele: hallo welt · alice OR bob · DATUM:heute · DATUM:2026-06-02",
+ "members_search_placeholder": "Mitglieder suchen...",
+ "members_search_no_results": "Keine Mitglieder entsprechen der Suche.",
+ "disabled_message": "Relay Chat ist in den Einstellungen deaktiviert. Aktiviere ihn unter Darstellung, um Hubs und Hosting zu nutzen.",
+ "ctx_add_hub": "Hub hinzufügen",
+ "ctx_add_room": "Raum beitreten",
+ "ctx_connect_hub": "Verbinden",
+ "ctx_disconnect_hub": "Trennen",
+ "ctx_hub_settings": "Hub-Einstellungen",
+ "ctx_remove_hub": "Hub entfernen",
+ "ctx_leave_room": "Raum verlassen",
+ "ctx_reply_quote": "Mit Zitat antworten",
+ "ctx_mention_user": "Benutzer erwähnen",
+ "ctx_copy_message": "Nachricht kopieren",
+ "ctx_kick_user": "Benutzer entfernen",
+ "ctx_ban_user": "Benutzer sperren",
+ "collapse_sidebar": "Seitenleiste einklappen",
+ "expand_sidebar": "Seitenleiste ausklappen",
+ "add_hub_card": "Hub hinzufügen",
+ "hub_display_name": "Anzeigename",
+ "revert_hub_name": "Zum angekündigten Namen zurücksetzen",
+ "search_messages": "Suchen",
+ "search_no_results": "Keine Nachrichten entsprechen der Suche",
+ "popout_channel": "In neuem Fenster öffnen",
+ "new_message_toast": "Neue Nachricht in #{room}"
}
}

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index f8117cc1..95d05f04 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -2238,7 +2238,11 @@
"host_members_select": "Select a member to view messages and moderate",
"host_no_messages": "No messages from this user in the log yet",
"host_ban_hub": "Ban from hub",
- "host_kick_confirm": "Kick {name} from the current room?",
+ "host_kick_confirm": "Kick {name} from #{room}?",
+ "host_kick_pick_room": "Kick {name} from which room? Enter one of: {rooms}",
+ "host_kick_no_room": "This user is not in any room, so they cannot be kicked.",
+ "host_kick_room_invalid": "Room not found. Choose one of the listed rooms.",
+ "host_cannot_moderate_self": "You cannot moderate your own connection.",
"host_ban_confirm": "Ban {name} from this hub?",
"host_room_ban_confirm": "Ban {name} from #{room}?",
"host_manage_rooms": "Manage rooms",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 7d896524..cf5e8ee9 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -2881,22 +2881,48 @@
"show_members": "Mostrar miembros",
"hide_members": "Ocultar miembros",
"discovery_refreshed": "Descubrimiento actualizado",
- "host_members_search": "Search members by name, hash, or room...",
- "host_members_select": "Select a member to view messages and moderate",
- "host_no_messages": "No messages from this user in the log yet",
- "host_ban_hub": "Ban from hub",
- "host_kick_confirm": "Kick {name} from the current room?",
- "host_ban_confirm": "Ban {name} from this hub?",
- "host_room_ban_confirm": "Ban {name} from #{room}?",
- "host_manage_rooms": "Manage rooms",
- "host_rooms_modal_title": "Rooms — {hub}",
- "host_rooms_select": "Select a room to view activity",
- "host_room_activity": "Recent activity",
- "host_no_activity": "No recent activity in this room",
- "search_messages_placeholder": "Search (AND, OR, DATE:2026-06-02)...",
- "search_empty_hint": "Search this room",
- "search_syntax_hint": "Examples: hello world · alice OR bob · DATE:today · DATE:2026-06-02",
- "members_search_placeholder": "Search members...",
- "members_search_no_results": "No members match your search."
+ "host_members_search": "Buscar miembros por nombre, hash o sala...",
+ "host_members_select": "Selecciona un miembro para ver mensajes y moderar",
+ "host_no_messages": "Aún no hay mensajes de este usuario en el registro",
+ "host_ban_hub": "Prohibir en el hub",
+ "host_kick_confirm": "¿Expulsar a {name} de #{room}?",
+ "host_kick_pick_room": "¿De qué sala expulsar a {name}? Introduce una de: {rooms}",
+ "host_kick_no_room": "Este usuario no está en ninguna sala y no puede ser expulsado.",
+ "host_kick_room_invalid": "Sala no encontrada. Elige una de las salas listadas.",
+ "host_cannot_moderate_self": "No puedes moderar tu propia conexión.",
+ "host_ban_confirm": "¿Prohibir a {name} en este hub?",
+ "host_room_ban_confirm": "¿Prohibir a {name} en #{room}?",
+ "host_manage_rooms": "Gestionar salas",
+ "host_rooms_modal_title": "Salas — {hub}",
+ "host_rooms_select": "Selecciona una sala para ver la actividad",
+ "host_room_activity": "Actividad reciente",
+ "host_no_activity": "No hay actividad reciente en esta sala",
+ "search_messages_placeholder": "Buscar (AND, OR, FECHA:2026-06-02)...",
+ "search_empty_hint": "Buscar en esta sala",
+ "search_syntax_hint": "Ejemplos: hola mundo · alice OR bob · FECHA:hoy · FECHA:2026-06-02",
+ "members_search_placeholder": "Buscar miembros...",
+ "members_search_no_results": "Ningún miembro coincide con la búsqueda.",
+ "disabled_message": "Relay Chat está desactivado en Ajustes. Actívalo en Apariencia para usar hubs y alojamiento.",
+ "ctx_add_hub": "Añadir hub",
+ "ctx_add_room": "Unirse a la sala",
+ "ctx_connect_hub": "Conectar",
+ "ctx_disconnect_hub": "Desconectar",
+ "ctx_hub_settings": "Ajustes del hub",
+ "ctx_remove_hub": "Eliminar hub",
+ "ctx_leave_room": "Salir de la sala",
+ "ctx_reply_quote": "Responder con cita",
+ "ctx_mention_user": "Mencionar usuario",
+ "ctx_copy_message": "Copiar mensaje",
+ "ctx_kick_user": "Expulsar usuario",
+ "ctx_ban_user": "Prohibir usuario",
+ "collapse_sidebar": "Contraer barra lateral",
+ "expand_sidebar": "Expandir barra lateral",
+ "add_hub_card": "Añadir un hub",
+ "hub_display_name": "Nombre visible",
+ "revert_hub_name": "Volver al nombre anunciado",
+ "search_messages": "Buscar",
+ "search_no_results": "Ningún mensaje coincide con la búsqueda",
+ "popout_channel": "Abrir en una ventana nueva",
+ "new_message_toast": "Nuevo mensaje en #{room}"
}
}

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 2a7a067f..ce9d7ca2 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -2881,22 +2881,48 @@
"show_members": "Afficher les membres",
"hide_members": "Masquer les membres",
"discovery_refreshed": "Découverte actualisée",
- "host_members_search": "Search members by name, hash, or room...",
- "host_members_select": "Select a member to view messages and moderate",
- "host_no_messages": "No messages from this user in the log yet",
- "host_ban_hub": "Ban from hub",
- "host_kick_confirm": "Kick {name} from the current room?",
- "host_ban_confirm": "Ban {name} from this hub?",
- "host_room_ban_confirm": "Ban {name} from #{room}?",
- "host_manage_rooms": "Manage rooms",
- "host_rooms_modal_title": "Rooms — {hub}",
- "host_rooms_select": "Select a room to view activity",
- "host_room_activity": "Recent activity",
- "host_no_activity": "No recent activity in this room",
- "search_messages_placeholder": "Search (AND, OR, DATE:2026-06-02)...",
- "search_empty_hint": "Search this room",
- "search_syntax_hint": "Examples: hello world · alice OR bob · DATE:today · DATE:2026-06-02",
- "members_search_placeholder": "Search members...",
- "members_search_no_results": "No members match your search."
+ "host_members_search": "Rechercher des membres par nom, hash ou salon...",
+ "host_members_select": "Sélectionnez un membre pour voir les messages et modérer",
+ "host_no_messages": "Aucun message de cet utilisateur dans le journal pour l'instant",
+ "host_ban_hub": "Bannir du hub",
+ "host_kick_confirm": "Expulser {name} de #{room} ?",
+ "host_kick_pick_room": "De quel salon expulser {name} ? Saisissez l'un de : {rooms}",
+ "host_kick_no_room": "Cet utilisateur n'est dans aucun salon et ne peut pas être expulsé.",
+ "host_kick_room_invalid": "Salon introuvable. Choisissez l'un des salons listés.",
+ "host_cannot_moderate_self": "Vous ne pouvez pas modérer votre propre connexion.",
+ "host_ban_confirm": "Bannir {name} de ce hub ?",
+ "host_room_ban_confirm": "Bannir {name} de #{room} ?",
+ "host_manage_rooms": "Gérer les salons",
+ "host_rooms_modal_title": "Salons — {hub}",
+ "host_rooms_select": "Sélectionnez un salon pour voir l'activité",
+ "host_room_activity": "Activité récente",
+ "host_no_activity": "Aucune activité récente dans ce salon",
+ "search_messages_placeholder": "Rechercher (AND, OR, DATE:2026-06-02)...",
+ "search_empty_hint": "Rechercher dans ce salon",
+ "search_syntax_hint": "Exemples : bonjour monde · alice OR bob · DATE:aujourd'hui · DATE:2026-06-02",
+ "members_search_placeholder": "Rechercher des membres...",
+ "members_search_no_results": "Aucun membre ne correspond à la recherche.",
+ "disabled_message": "Relay Chat est désactivé dans les paramètres. Activez-le sous Apparence pour utiliser les hubs et l'hébergement.",
+ "ctx_add_hub": "Ajouter un hub",
+ "ctx_add_room": "Rejoindre le salon",
+ "ctx_connect_hub": "Se connecter",
+ "ctx_disconnect_hub": "Se déconnecter",
+ "ctx_hub_settings": "Paramètres du hub",
+ "ctx_remove_hub": "Supprimer le hub",
+ "ctx_leave_room": "Quitter le salon",
+ "ctx_reply_quote": "Répondre avec citation",
+ "ctx_mention_user": "Mentionner l'utilisateur",
+ "ctx_copy_message": "Copier le message",
+ "ctx_kick_user": "Expulser l'utilisateur",
+ "ctx_ban_user": "Bannir l'utilisateur",
+ "collapse_sidebar": "Réduire la barre latérale",
+ "expand_sidebar": "Développer la barre latérale",
+ "add_hub_card": "Ajouter un hub",
+ "hub_display_name": "Nom affiché",
+ "revert_hub_name": "Revenir au nom annoncé",
+ "search_messages": "Rechercher",
+ "search_no_results": "Aucun message ne correspond à la recherche",
+ "popout_channel": "Ouvrir dans une nouvelle fenêtre",
+ "new_message_toast": "Nouveau message dans #{room}"
}
}

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index a63f4a31..391b73b2 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -2881,22 +2881,48 @@
"show_members": "Mostra membri",
"hide_members": "Nascondi membri",
"discovery_refreshed": "Scoperta aggiornata",
- "host_members_search": "Search members by name, hash, or room...",
- "host_members_select": "Select a member to view messages and moderate",
- "host_no_messages": "No messages from this user in the log yet",
- "host_ban_hub": "Ban from hub",
- "host_kick_confirm": "Kick {name} from the current room?",
- "host_ban_confirm": "Ban {name} from this hub?",
- "host_room_ban_confirm": "Ban {name} from #{room}?",
- "host_manage_rooms": "Manage rooms",
- "host_rooms_modal_title": "Rooms — {hub}",
- "host_rooms_select": "Select a room to view activity",
- "host_room_activity": "Recent activity",
- "host_no_activity": "No recent activity in this room",
- "search_messages_placeholder": "Search (AND, OR, DATE:2026-06-02)...",
- "search_empty_hint": "Search this room",
- "search_syntax_hint": "Examples: hello world · alice OR bob · DATE:today · DATE:2026-06-02",
- "members_search_placeholder": "Search members...",
- "members_search_no_results": "No members match your search."
+ "host_members_search": "Cerca membri per nome, hash o stanza...",
+ "host_members_select": "Seleziona un membro per vedere i messaggi e moderare",
+ "host_no_messages": "Nessun messaggio da questo utente nel registro",
+ "host_ban_hub": "Banna dall'hub",
+ "host_kick_confirm": "Espellere {name} da #{room}?",
+ "host_kick_pick_room": "Da quale stanza espellere {name}? Inserisci una di: {rooms}",
+ "host_kick_no_room": "Questo utente non è in nessuna stanza e non può essere espulso.",
+ "host_kick_room_invalid": "Stanza non trovata. Scegli una delle stanze elencate.",
+ "host_cannot_moderate_self": "Non puoi moderare la tua connessione.",
+ "host_ban_confirm": "Bannare {name} da questo hub?",
+ "host_room_ban_confirm": "Bannare {name} da #{room}?",
+ "host_manage_rooms": "Gestisci stanze",
+ "host_rooms_modal_title": "Stanze — {hub}",
+ "host_rooms_select": "Seleziona una stanza per vedere l'attività",
+ "host_room_activity": "Attività recente",
+ "host_no_activity": "Nessuna attività recente in questa stanza",
+ "search_messages_placeholder": "Cerca (AND, OR, DATA:2026-06-02)...",
+ "search_empty_hint": "Cerca in questa stanza",
+ "search_syntax_hint": "Esempi: ciao mondo · alice OR bob · DATA:oggi · DATA:2026-06-02",
+ "members_search_placeholder": "Cerca membri...",
+ "members_search_no_results": "Nessun membro corrisponde alla ricerca.",
+ "disabled_message": "Relay Chat è disattivato nelle Impostazioni. Abilitalo in Aspetto per usare hub e hosting.",
+ "ctx_add_hub": "Aggiungi hub",
+ "ctx_add_room": "Entra nella stanza",
+ "ctx_connect_hub": "Connetti",
+ "ctx_disconnect_hub": "Disconnetti",
+ "ctx_hub_settings": "Impostazioni hub",
+ "ctx_remove_hub": "Rimuovi hub",
+ "ctx_leave_room": "Esci dalla stanza",
+ "ctx_reply_quote": "Rispondi con citazione",
+ "ctx_mention_user": "Menziona utente",
+ "ctx_copy_message": "Copia messaggio",
+ "ctx_kick_user": "Espelli utente",
+ "ctx_ban_user": "Banna utente",
+ "collapse_sidebar": "Comprimi barra laterale",
+ "expand_sidebar": "Espandi barra laterale",
+ "add_hub_card": "Aggiungi un hub",
+ "hub_display_name": "Nome visualizzato",
+ "revert_hub_name": "Ripristina nome annunciato",
+ "search_messages": "Cerca",
+ "search_no_results": "Nessun messaggio corrisponde alla ricerca",
+ "popout_channel": "Apri in una nuova finestra",
+ "new_message_toast": "Nuovo messaggio in #{room}"
}
}

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 0593b737..b0f15c46 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -2881,22 +2881,48 @@
"show_members": "Leden tonen",
"hide_members": "Leden verbergen",
"discovery_refreshed": "Ontdekking vernieuwd",
- "host_members_search": "Search members by name, hash, or room...",
- "host_members_select": "Select a member to view messages and moderate",
- "host_no_messages": "No messages from this user in the log yet",
- "host_ban_hub": "Ban from hub",
- "host_kick_confirm": "Kick {name} from the current room?",
- "host_ban_confirm": "Ban {name} from this hub?",
- "host_room_ban_confirm": "Ban {name} from #{room}?",
- "host_manage_rooms": "Manage rooms",
- "host_rooms_modal_title": "Rooms — {hub}",
- "host_rooms_select": "Select a room to view activity",
- "host_room_activity": "Recent activity",
- "host_no_activity": "No recent activity in this room",
- "search_messages_placeholder": "Search (AND, OR, DATE:2026-06-02)...",
- "search_empty_hint": "Search this room",
- "search_syntax_hint": "Examples: hello world · alice OR bob · DATE:today · DATE:2026-06-02",
- "members_search_placeholder": "Search members...",
- "members_search_no_results": "No members match your search."
+ "host_members_search": "Zoek leden op naam, hash of kamer...",
+ "host_members_select": "Selecteer een lid om berichten te bekijken en te modereren",
+ "host_no_messages": "Nog geen berichten van deze gebruiker in het logboek",
+ "host_ban_hub": "Verbannen van hub",
+ "host_kick_confirm": "{name} uit #{room} verwijderen?",
+ "host_kick_pick_room": "Uit welke kamer moet {name} worden verwijderd? Voer een van in: {rooms}",
+ "host_kick_no_room": "Deze gebruiker zit in geen kamer en kan niet worden verwijderd.",
+ "host_kick_room_invalid": "Kamer niet gevonden. Kies een van de vermelde kamers.",
+ "host_cannot_moderate_self": "Je kunt je eigen verbinding niet modereren.",
+ "host_ban_confirm": "{name} van deze hub verbannen?",
+ "host_room_ban_confirm": "{name} uit #{room} verbannen?",
+ "host_manage_rooms": "Kamers beheren",
+ "host_rooms_modal_title": "Kamers — {hub}",
+ "host_rooms_select": "Selecteer een kamer om activiteit te bekijken",
+ "host_room_activity": "Recente activiteit",
+ "host_no_activity": "Geen recente activiteit in deze kamer",
+ "search_messages_placeholder": "Zoeken (AND, OR, DATUM:2026-06-02)...",
+ "search_empty_hint": "Deze kamer doorzoeken",
+ "search_syntax_hint": "Voorbeelden: hallo wereld · alice OR bob · DATUM:vandaag · DATUM:2026-06-02",
+ "members_search_placeholder": "Leden zoeken...",
+ "members_search_no_results": "Geen leden komen overeen met je zoekopdracht.",
+ "disabled_message": "Relay Chat staat uit in Instellingen. Schakel het in onder Weergave om hubs en hosting te gebruiken.",
+ "ctx_add_hub": "Hub toevoegen",
+ "ctx_add_room": "Kamer joinen",
+ "ctx_connect_hub": "Verbinden",
+ "ctx_disconnect_hub": "Verbreken",
+ "ctx_hub_settings": "Hub-instellingen",
+ "ctx_remove_hub": "Hub verwijderen",
+ "ctx_leave_room": "Kamer verlaten",
+ "ctx_reply_quote": "Antwoorden met citaat",
+ "ctx_mention_user": "Gebruiker vermelden",
+ "ctx_copy_message": "Bericht kopiëren",
+ "ctx_kick_user": "Gebruiker verwijderen",
+ "ctx_ban_user": "Gebruiker verbannen",
+ "collapse_sidebar": "Zijbalk inklappen",
+ "expand_sidebar": "Zijbalk uitklappen",
+ "add_hub_card": "Hub toevoegen",
+ "hub_display_name": "Weergavenaam",
+ "revert_hub_name": "Terug naar aangekondigde naam",
+ "search_messages": "Zoeken",
+ "search_no_results": "Geen berichten komen overeen met je zoekopdracht",
+ "popout_channel": "Openen in nieuw venster",
+ "new_message_toast": "Nieuw bericht in #{room}"
}
}

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 10e13484..da510c7a 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -2881,22 +2881,48 @@
"show_members": "Показать участников",
"hide_members": "Скрыть участников",
"discovery_refreshed": "Обзор обновлён",
- "host_members_search": "Search members by name, hash, or room...",
- "host_members_select": "Select a member to view messages and moderate",
- "host_no_messages": "No messages from this user in the log yet",
- "host_ban_hub": "Ban from hub",
- "host_kick_confirm": "Kick {name} from the current room?",
- "host_ban_confirm": "Ban {name} from this hub?",
- "host_room_ban_confirm": "Ban {name} from #{room}?",
- "host_manage_rooms": "Manage rooms",
- "host_rooms_modal_title": "Rooms — {hub}",
- "host_rooms_select": "Select a room to view activity",
- "host_room_activity": "Recent activity",
- "host_no_activity": "No recent activity in this room",
- "search_messages_placeholder": "Search (AND, OR, DATE:2026-06-02)...",
- "search_empty_hint": "Search this room",
- "search_syntax_hint": "Examples: hello world · alice OR bob · DATE:today · DATE:2026-06-02",
- "members_search_placeholder": "Search members...",
- "members_search_no_results": "No members match your search."
+ "host_members_search": "Поиск участников по имени, хешу или комнате...",
+ "host_members_select": "Выберите участника, чтобы просмотреть сообщения и модерировать",
+ "host_no_messages": "В журнале пока нет сообщений от этого пользователя",
+ "host_ban_hub": "Забанить на хабе",
+ "host_kick_confirm": "Исключить {name} из #{room}?",
+ "host_kick_pick_room": "Из какой комнаты исключить {name}? Введите одну из: {rooms}",
+ "host_kick_no_room": "Этот пользователь не в комнате и не может быть исключён.",
+ "host_kick_room_invalid": "Комната не найдена. Выберите одну из перечисленных.",
+ "host_cannot_moderate_self": "Нельзя модерировать собственное подключение.",
+ "host_ban_confirm": "Забанить {name} на этом хабе?",
+ "host_room_ban_confirm": "Забанить {name} в #{room}?",
+ "host_manage_rooms": "Управление комнатами",
+ "host_rooms_modal_title": "Комнаты — {hub}",
+ "host_rooms_select": "Выберите комнату для просмотра активности",
+ "host_room_activity": "Недавняя активность",
+ "host_no_activity": "Нет недавней активности в этой комнате",
+ "search_messages_placeholder": "Поиск (AND, OR, DATE:2026-06-02)...",
+ "search_empty_hint": "Искать в этой комнате",
+ "search_syntax_hint": "Примеры: привет мир · alice OR bob · DATE:сегодня · DATE:2026-06-02",
+ "members_search_placeholder": "Поиск участников...",
+ "members_search_no_results": "Участники по запросу не найдены.",
+ "disabled_message": "Relay Chat отключён в настройках. Включите его в разделе «Внешний вид», чтобы использовать хабы и хостинг.",
+ "ctx_add_hub": "Добавить хаб",
+ "ctx_add_room": "Войти в комнату",
+ "ctx_connect_hub": "Подключиться",
+ "ctx_disconnect_hub": "Отключиться",
+ "ctx_hub_settings": "Настройки хаба",
+ "ctx_remove_hub": "Удалить хаб",
+ "ctx_leave_room": "Покинуть комнату",
+ "ctx_reply_quote": "Ответить с цитатой",
+ "ctx_mention_user": "Упомянуть пользователя",
+ "ctx_copy_message": "Копировать сообщение",
+ "ctx_kick_user": "Исключить пользователя",
+ "ctx_ban_user": "Забанить пользователя",
+ "collapse_sidebar": "Свернуть боковую панель",
+ "expand_sidebar": "Развернуть боковую панель",
+ "add_hub_card": "Добавить хаб",
+ "hub_display_name": "Отображаемое имя",
+ "revert_hub_name": "Вернуть объявленное имя",
+ "search_messages": "Поиск",
+ "search_no_results": "Сообщения по запросу не найдены",
+ "popout_channel": "Открыть в новом окне",
+ "new_message_toast": "Новое сообщение в #{room}"
}
}

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index 4c480af2..7b3968d1 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -2881,22 +2881,48 @@
"show_members": "显示成员",
"hide_members": "隐藏成员",
"discovery_refreshed": "已刷新发现",
- "host_members_search": "Search members by name, hash, or room...",
- "host_members_select": "Select a member to view messages and moderate",
- "host_no_messages": "No messages from this user in the log yet",
- "host_ban_hub": "Ban from hub",
- "host_kick_confirm": "Kick {name} from the current room?",
- "host_ban_confirm": "Ban {name} from this hub?",
- "host_room_ban_confirm": "Ban {name} from #{room}?",
- "host_manage_rooms": "Manage rooms",
- "host_rooms_modal_title": "Rooms — {hub}",
- "host_rooms_select": "Select a room to view activity",
- "host_room_activity": "Recent activity",
- "host_no_activity": "No recent activity in this room",
- "search_messages_placeholder": "Search (AND, OR, DATE:2026-06-02)...",
- "search_empty_hint": "Search this room",
- "search_syntax_hint": "Examples: hello world · alice OR bob · DATE:today · DATE:2026-06-02",
- "members_search_placeholder": "Search members...",
- "members_search_no_results": "No members match your search."
+ "host_members_search": "按名称、哈希或房间搜索成员...",
+ "host_members_select": "选择成员以查看消息并进行管理",
+ "host_no_messages": "日志中尚无此用户的消息",
+ "host_ban_hub": "从中心封禁",
+ "host_kick_confirm": "将 {name} 从 #{room} 踢出?",
+ "host_kick_pick_room": "从哪个房间踢出 {name}?请输入其中之一:{rooms}",
+ "host_kick_no_room": "此用户不在任何房间,无法踢出。",
+ "host_kick_room_invalid": "未找到房间。请选择列出的房间之一。",
+ "host_cannot_moderate_self": "无法管理自己的连接。",
+ "host_ban_confirm": "在中心封禁 {name}?",
+ "host_room_ban_confirm": "在 #{room} 封禁 {name}?",
+ "host_manage_rooms": "管理房间",
+ "host_rooms_modal_title": "房间 — {hub}",
+ "host_rooms_select": "选择房间以查看活动",
+ "host_room_activity": "最近活动",
+ "host_no_activity": "此房间暂无最近活动",
+ "search_messages_placeholder": "搜索(AND、OR、DATE:2026-06-02)...",
+ "search_empty_hint": "搜索此房间",
+ "search_syntax_hint": "示例:hello world · alice OR bob · DATE:today · DATE:2026-06-02",
+ "members_search_placeholder": "搜索成员...",
+ "members_search_no_results": "没有匹配搜索的成员。",
+ "disabled_message": "中继聊天已在设置中关闭。请在「外观」中启用以使用中心和托管。",
+ "ctx_add_hub": "添加中心",
+ "ctx_add_room": "加入房间",
+ "ctx_connect_hub": "连接",
+ "ctx_disconnect_hub": "断开连接",
+ "ctx_hub_settings": "中心设置",
+ "ctx_remove_hub": "移除中心",
+ "ctx_leave_room": "离开房间",
+ "ctx_reply_quote": "引用回复",
+ "ctx_mention_user": "提及用户",
+ "ctx_copy_message": "复制消息",
+ "ctx_kick_user": "踢出用户",
+ "ctx_ban_user": "封禁用户",
+ "collapse_sidebar": "折叠侧边栏",
+ "expand_sidebar": "展开侧边栏",
+ "add_hub_card": "添加中心",
+ "hub_display_name": "显示名称",
+ "revert_hub_name": "恢复为公告名称",
+ "search_messages": "搜索",
+ "search_no_results": "没有匹配搜索的消息",
+ "popout_channel": "在新窗口中打开",
+ "new_message_toast": "#{room} 中有新消息"
}
}

diff --git a/meshchatx/src/frontend/main.js b/meshchatx/src/frontend/main.js
index 8933f552..a9c0d967 100644
--- a/meshchatx/src/frontend/main.js
+++ b/meshchatx/src/frontend/main.js
@@ -116,6 +116,7 @@ const router = createRouter({
name: "nomadnetwork",
path: "/nomadnetwork/:destinationHash?",
props: true,
+ meta: { keepAlive: true },
component: defineAsyncComponent(() => import("./components/nomadnetwork/NomadNetworkBrowser.vue")),
},
{

diff --git a/tests/frontend/RelayHostMembersModal.test.js b/tests/frontend/RelayHostMembersModal.test.js
new file mode 100644
index 00000000..dfa0b41c
--- /dev/null
+++ b/tests/frontend/RelayHostMembersModal.test.js
@@ -0,0 +1,105 @@
+import { mount } from "@vue/test-utils";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import RelayHostMembersModal from "@/components/relay/RelayHostMembersModal.vue";
+import DialogUtils from "@/js/DialogUtils";
+import ToastUtils from "@/js/ToastUtils";
+import { mountToolsPageGlobals } from "./testI18n.js";
+
+vi.mock("@/js/DialogUtils", () => ({
+ default: {
+ confirm: vi.fn(),
+ prompt: vi.fn(),
+ },
+}));
+
+vi.mock("@/js/ToastUtils", () => ({
+ default: {
+ success: vi.fn(),
+ error: vi.fn(),
+ warning: vi.fn(),
+ },
+}));
+
+const HUB_ID = "deadbeefdeadbeefdeadbeefdeadbeef";
+const PEER_HASH = "00112233445566778899aabbccddeeff";
+const LOCAL_HASH = "ffeeddccbbaa99887766554433221100";
+
+function makeHub() {
+ return { id: HUB_ID, name: "Hosted" };
+}
+
+function makeMember(overrides = {}) {
+ return {
+ hash: PEER_HASH,
+ name: "alice",
+ rooms: ["lobby"],
+ ...overrides,
+ };
+}
+
+describe("RelayHostMembersModal.vue", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ DialogUtils.confirm.mockResolvedValue(true);
+ window.api = {
+ get: vi.fn(async (url) => {
+ if (url === "/api/v1/config") {
+ return { data: { identity_hash: LOCAL_HASH } };
+ }
+ if (url.includes("/members")) {
+ return { data: { members: [makeMember()] } };
+ }
+ return { data: {} };
+ }),
+ post: vi.fn(async () => ({ data: { message: "ok" } })),
+ };
+ });
+
+ const mountModal = (props = {}) =>
+ mount(RelayHostMembersModal, {
+ props: {
+ open: true,
+ hub: makeHub(),
+ room: null,
+ ...props,
+ },
+ global: mountToolsPageGlobals(),
+ });
+
+ it("kicks using the member room when the modal has no room filter", async () => {
+ const wrapper = mountModal();
+ await wrapper.vm.fetchMembers();
+
+ await wrapper.vm.moderate(makeMember(), "kick");
+
+ expect(window.api.post).toHaveBeenCalledWith(`/api/v1/rrc/servers/${HUB_ID}/moderate`, {
+ action: "kick",
+ peer: PEER_HASH,
+ room: "lobby",
+ });
+ });
+
+ it("uses the modal room filter when set", async () => {
+ const wrapper = mountModal({ room: "ops" });
+ await wrapper.vm.fetchMembers();
+
+ await wrapper.vm.moderate(makeMember({ rooms: ["lobby", "ops"] }), "kick");
+
+ expect(window.api.post).toHaveBeenCalledWith(`/api/v1/rrc/servers/${HUB_ID}/moderate`, {
+ action: "kick",
+ peer: PEER_HASH,
+ room: "ops",
+ });
+ });
+
+ it("blocks moderating the local identity", async () => {
+ const wrapper = mountModal();
+ await wrapper.vm.ensureLocalIdentity();
+
+ await wrapper.vm.moderate(makeMember({ hash: LOCAL_HASH }), "kick");
+
+ expect(window.api.post).not.toHaveBeenCalled();
+ expect(ToastUtils.warning).toHaveBeenCalled();
+ });
+});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────